using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Timers;
using Timer=System.Timers.Timer;

namespace Fadd.Globalization.Yaml
{
    /// <summary>
    /// Class to subscribe to change events on a file and update a language hierarchy every time the file changes.
    /// </summary>
    /// <remarks>
    /// If the file being watched is deleted or renamed no more changes will be made to the language node.
    /// Also be observant on the fact that the class cannot Remove entries from a <see cref="LanguageNode"/>, only add or modify entries.
    /// This restriction is needed because a single YamlWatcher only watches One file and a <see cref="LanguageNode"/> can be connected to several
    /// watchers and files, deleting anything could clear things for another file.
    /// </remarks>
    public class YamlWatcher : IDisposable, ILanguageWatcher
    {
        private readonly string _path;
        private FileSystemWatcher _watcher;
        private readonly LanguageNode _rootNode;
        private readonly Dictionary<string, FileMapping> _files = new Dictionary<string, FileMapping>();

        /// <summary>
        /// files that need to be reloaded
        /// </summary>
        private readonly Queue<string> _filesToReload = new Queue<string>();

        private class FileMapping
        {
            public readonly LanguageNode Language;
            public readonly string FullPath;
            public FileMapping(LanguageNode node, string path)
            {
                Language = node;
                FullPath = path;
            }
        }

        /// <summary>A timer to use as delay if the file is currently in use</summary>
        private readonly Timer _readTimer = new Timer(5000);

        /// <summary>
        /// Instantiates the class to listen to changes in a file, also reads the file and fills the language node with language entries
        /// </summary>
        /// <param name="languageNode">Language node to fill</param>
        /// <param name="filename">The filename to watch</param>
        /// <exception cref="FileNotFoundException">Thrown if the specified file does not exist</exception>
        public YamlWatcher(LanguageNode languageNode, string filename)
        {
            if (!File.Exists(filename))
                throw new FileNotFoundException(filename + " do not exist.", filename);

            string fullPath = Path.GetFullPath(filename);
            _path = Path.GetDirectoryName(fullPath);
            _rootNode = languageNode;
            _files.Add(Path.GetFileName(filename), new FileMapping(_rootNode, fullPath));
            LoadFile(filename, _rootNode);
            Watch();

            _readTimer.Elapsed += OnTryRead;
        }

        /// <summary>
        /// Root language node.
        /// </summary>
        /// <value></value>
    	public LanguageNode RootNode
    	{
			get { return _rootNode; }
    	}


        /// <summary>
        /// Load and watch another file on disk.
        /// </summary>
        /// <param name="filename">Relative or absolute path to language file.</param>
        /// <returns>Node that have been loaded.</returns>
        public ILanguageNode Add(string filename)
        {
            string nodeName = Path.GetFileNameWithoutExtension(filename);
            LanguageNode node = _rootNode.AddChild(nodeName);
            LoadFile(filename, node);
            _files.Add(Path.GetFileName(filename), new FileMapping(_rootNode, Path.GetFullPath(filename)));
            return node;
        }

		/// <summary>
		/// Parse YAML contents and add any language nodes to the watchers language
		/// </summary>
		/// <param name="yaml">YAML contents</param>
		/// <param name="nodeName">The name of the node to add the contents into</param>
		/// <returns>Node that have been loaded.</returns>
    	public ILanguageNode AddContents(string yaml, string nodeName)
    	{
			MemLanguageNode node = new MemLanguageNode(Thread.CurrentThread.CurrentCulture.LCID, nodeName);
    		using(TextReader reader = new StringReader(yaml))
				ParseStream(reader, node);

    		return node;
    	}

    	/// <summary>
        /// Callback for when the file should be read again
        /// </summary>
        void OnTryRead(object sender, ElapsedEventArgs e)
        {
            string filename;
            lock (_filesToReload)
                filename = _filesToReload.Count != 0 ? _filesToReload.Dequeue() : null;

            string failedFile = null;
            while (filename != null && failedFile != filename)
            {
                if (!LoadFile(filename) && failedFile == null)
                    failedFile = filename;

                lock (_filesToReload)
                    filename = _filesToReload.Count != 0 ? _filesToReload.Dequeue() : null;
            }
        }

        /// <summary>
        /// Sets the class to watch the specified file
        /// </summary>
        private void Watch()
        {
            _watcher = new FileSystemWatcher(_path, "*.yaml") {EnableRaisingEvents = true};
        	_watcher.Changed += OnFileChanged;
        }

        /// <summary>
        /// Callback for when the file changes
        /// </summary>
        private void OnFileChanged(object sender, FileSystemEventArgs e)
        {
            LoadFile(e.FullPath);
        }

    	private static void ParseStream(TextReader reader, LanguageNode rootNode)
		{
			Check.Require(rootNode, "rootNode");
			Check.False(rootNode is EmptyLanguageNode, "rootNode must not be of type Empty");

			YamlLight yaml = YamlLight.Parse(reader);
			
			// go through all languages
			foreach (YamlLight language in yaml)
			{
				int lcid;
				if (!int.TryParse(language.Name, out lcid)) continue;
				// go through all textstrings / categories for each language
				foreach (YamlLight childNode in language)
				{
					// if the entry contains more than one line of text it is a sub root node and not just an entry in the root
					if (childNode.Count > 0)
					{
						// try to retrieve the node from the root in case it's been instantiated before otherwise create it then expand
						LanguageNode languageNode =
							(LanguageNode) (rootNode.GetChild(childNode.Name) ?? rootNode.AddChild(childNode.Name));

						foreach (YamlLight child in childNode)
							Expand(child, languageNode, lcid);

						if (!string.IsNullOrEmpty(childNode.Value))
							rootNode.Set(childNode.Name, lcid, childNode.Value);
					}
					else
						rootNode.Set(childNode.Name, lcid, childNode.Value);
				}
			}
		}

        /// <summary>
        /// Private loading of file to wrap read failure control
        /// </summary>
        private bool LoadFile(string filename)
        {
            try
            {
                FileMapping mapping;
                if (_files.TryGetValue(Path.GetFileName(filename), out mapping))
                    LoadFile(mapping.FullPath, mapping.Language);
                return true;
            }
            catch (IOException)
            {
                lock (_filesToReload)
                    _filesToReload.Enqueue(filename);
                return false;
            }
        }

        /// <summary>
        /// Fill the root node with languages and categories from the specified file
        /// </summary>
        /// <param name="fullPath">Full file path</param>
        /// <param name="rootNode">The root node to fill</param>
        public static void LoadFile(string fullPath, LanguageNode rootNode)
        {
            Check.NotEmpty(fullPath, "fullPath");

            // Clear out any old information in the language nodes
            // rootNode.ClearHierarchy(); // todo, decide what to do with this

            //TextReader reader = new StreamReader(fullPath);
            using (Stream stream = new FileStream(fullPath, FileMode.Open, FileAccess.Read, FileShare.None))
				using (TextReader reader = new StreamReader(stream))
					ParseStream(reader, rootNode);
        }

        private static void Expand(YamlLight node, LanguageNode parent, int lcid)
        {
            if(node.Count > 0)
            {
                ILanguageNode langNode = parent.GetChild(node.Name) ?? parent.AddChild(node.Name);

                if (!string.IsNullOrEmpty(node.Value))
					parent.Set(node.Name, lcid, node.Value);

                foreach (YamlLight subNode in node)
                    Expand(subNode, (LanguageNode)langNode, lcid);
            }
            else
                parent.Set(node.Name, lcid, node.Value);
        }

        #region IDisposable members
        /// <summary>
        /// Function to stop the watcher
        /// </summary>
        public void Dispose()
        {
            if (_watcher != null)
            {
                _watcher.Dispose();
                _watcher = null;
            }
        }
        #endregion
    }
}